// Copyright 2007, 2008, 2009, 2010, 2011 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.hxzon.tapestry5.components;

import java.io.IOException;
import java.util.Collections;
import java.util.List;

import org.apache.tapestry5.Binding;
import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.Block;
import org.apache.tapestry5.ComponentParameterConstants;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.PropertyOverrides;
import org.apache.tapestry5.ValueEncoder;
import org.apache.tapestry5.annotations.Component;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.OnEvent;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.annotations.SupportsInformalParameters;
import org.apache.tapestry5.beaneditor.BeanModel;
import org.apache.tapestry5.beaneditor.PropertyModel;
import org.apache.tapestry5.corelib.components.Any;
import org.apache.tapestry5.corelib.components.Delegate;
import org.apache.tapestry5.corelib.data.GridPagerPosition;
import org.apache.tapestry5.grid.ColumnSort;
import org.apache.tapestry5.grid.GridDataSource;
import org.apache.tapestry5.grid.GridModel;
import org.apache.tapestry5.grid.GridSortModel;
import org.apache.tapestry5.grid.SortConstraint;
import org.apache.tapestry5.internal.TapestryInternalUtils;
import org.apache.tapestry5.internal.beaneditor.BeanModelUtils;
import org.apache.tapestry5.internal.bindings.AbstractBinding;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.services.BeanModelSource;
import org.apache.tapestry5.services.ClientBehaviorSupport;
import org.apache.tapestry5.services.ComponentDefaultProvider;
import org.apache.tapestry5.services.ComponentEventResultProcessor;
import org.apache.tapestry5.services.FormSupport;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;
import org.hxzon.tapestry5.util.TapestryEx;
import org.slf4j.Logger;

@SupportsInformalParameters
public class GridEx implements GridModel {
    @Inject
    private Logger logger;
    //
    @Parameter(required = true, autoconnect = true)
    private GridDataSource source;

    @Parameter(value = "1")
    @Property
    private long currentPage;

    @Parameter(value = "1")
    @Property
    private long maxPage;

    @Parameter(value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_PAGER_POSITION, defaultPrefix = BindingConstants.LITERAL)
    private GridPagerPosition pagerPosition;

    @Parameter(principal = true)
    @Property
    private Object row;

    @Parameter
    private int columnIndex;

    @Parameter
    private BeanModel<?> model;

    private BeanModel<?> dataModel;

    @Parameter
    private GridSortModel sortModel;

    //
    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    private String add;

    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    private String include;

    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    private String exclude;

    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    private String reorder;

    //@Parameter(value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_EMPTY_BLOCK,
    @Parameter(value = "block:empty", defaultPrefix = BindingConstants.LITERAL)
    private Block empty;

    @Parameter(name = "class", defaultPrefix = BindingConstants.LITERAL, value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_TABLE_CSS_CLASS)
    @Property(write = false)
    private String tableClass;

    @Parameter
    private boolean inPlace;

    @Parameter(value = "this", allowNull = false)
    @Property(write = false)
    private PropertyOverrides overrides;

    @Property(write = false)
    private String zone;

    private boolean didRenderZoneDiv;

    @Persist
    private String sortColumnId;

    @Persist
    private Boolean sortAscending;

    @Inject
    private ComponentResources resources;

    @Inject
    private BeanModelSource modelSource;

    @Environmental
    private ClientBehaviorSupport clientBehaviorSupport;

    @Component(parameters = { "index=inherit:columnIndex", "lean=inherit:lean", "overrides=overrides", "zone=zone" })
    private GridExColumns columns;

    @Component(parameters = { "columnIndex=inherit:columnIndex", "row=row", "overrides=overrides" }, //
    publishParameters = "rowIndex,rowClass,volatile,encoder,lean")
    private GridExRows rows;

    @Component(parameters = { "maxPage=maxPage", "currentPage=currentPage", "zone=zone" })
    private GridExPager pager;

    @Component(parameters = "to=pagerTop")
    private Delegate pagerTop;

    @Component(parameters = "to=pagerBottom")
    private Delegate pagerBottom;

    @Component(parameters = "class=tableClass", inheritInformalParameters = true)
    private Any table;

    @Environmental(false)
    private FormSupport formSupport;

    @Environmental
    private JavaScriptSupport jsSupport;

    @Environmental
    private ComponentEventResultProcessor<GridEx> componentEventResultProcessor;

    @Inject
    private ComponentDefaultProvider defaultsProvider;

    ValueEncoder<?> defaultEncoder() {
        return defaultsProvider.defaultValueEncoder("row", resources);
    }

    GridSortModel defaultSortModel() {
        return new DefaultGridSortModel();
    }

    protected Binding defaultModel() {
        return new AbstractBinding() {
            public Object get() {
                // Get the default row type from the data source

                GridDataSource gridDataSource = source;

                Class<?> rowType = gridDataSource.getRowType();

                if (rowType == null) {
                    throw new RuntimeException(String.format("Unable to determine the bean type for rows from %s. You should bind the model parameter explicitly.", gridDataSource));
                }
                // Properties do not have to be read/write

                return modelSource.createDisplayModel(rowType, overrides.getOverrideMessages());
            }

            @Override
            public boolean isInvariant() {
                return false;
            }
        };
    }

    Object setupRender() {
        logger.trace("setup render");
        // If there's no rows, display the empty block placeholder.
        return source.getAvailableRows() == 0 ? empty : null;
    }

    Object beginRender(MarkupWriter writer) {
        logger.trace("begin render");
        // Skip rendering of component (template, body, etc.) when there's nothing to display.
        // The empty placeholder will already have rendered.

        if (source.getAvailableRows() == 0) {
            return false;
        }

        if (inPlace && zone == null) {
            zone = jsSupport.allocateClientId(resources);

            writer.element("div", "id", zone);

            clientBehaviorSupport.addZone(zone, null, "show");

            didRenderZoneDiv = true;
        }

        return null;
    }

    void afterRender(MarkupWriter writer) {
        logger.trace("after render");
        if (didRenderZoneDiv) {
            writer.end(); // div
            didRenderZoneDiv = false;
        }
    }

    public BeanModel<?> getDataModel() {
        if (dataModel == null) {
            dataModel = model;

            BeanModelUtils.modify(dataModel, add, include, exclude, reorder);
        }

        return dataModel;
    }

    public GridDataSource getDataSource() {
        return source;
    }

    public GridSortModel getSortModel() {
        return sortModel;
    }

    public Object getPagerTop() {
        return pagerPosition.isMatchTop() ? pager : null;
    }

    public Object getPagerBottom() {
        return pagerPosition.isMatchBottom() ? pager : null;
    }

    private boolean getSortAscending() {
        return sortAscending != null && sortAscending.booleanValue();
    }

    private void setSortAscending(boolean sortAscending) {
        this.sortAscending = sortAscending;
    }

    /**
     * Resets the Grid to inital settings; this sets the current page to one, and
     * {@linkplain org.apache.tapestry5.grid.GridSortModel#clear() clears the sort model}.
     */
    public void reset() {
        currentPage = 1L;
        sortModel.clear();
    }

    /**
     * Event handler for inplaceupdate event triggered from nested components when an Ajax update occurs. The event
     * context will carry the zone, which is recorded here, to allow the Grid and its sub-components to properly
     * re-render themselves. Invokes
     * {@link org.apache.tapestry5.services.ComponentEventResultProcessor#processResultValue(Object)} passing this (the
     * Grid component) as the content provider for the update.
     */
    @OnEvent(value = TapestryEx.Event_GridInplaceUpdate)
    void onInPlaceUpdate(String zone) throws IOException {
        this.zone = zone;

        componentEventResultProcessor.processResultValue(this);
    }

    /**
     * Normal, non-Ajax event handler.
     */
    void onAction(long newPage) {
        logger.trace("onAction({})", newPage);
        currentPage = newPage;
    }

    /**
     * Ajax event handler, passing the zone along.
     */
    boolean onAction(long newPage, String zone) {
        logger.trace("onAction({},{})", newPage, zone);
        currentPage = newPage;
        //for page prepare source
        resources.triggerEvent(TapestryEx.Event_BeforeGridInplaceUpdate, new Object[] { zone }, null);
        //
        resources.triggerEvent(TapestryEx.Event_GridInplaceUpdate, new Object[] { zone }, null);

        return false; // abort event
    }

    /**
     * Default implementation that only allows a single column to be the sort column, and stores the sort information as
     * persistent fields of the Grid component.
     */
    class DefaultGridSortModel implements GridSortModel {
        public ColumnSort getColumnSort(String columnId) {
            if (!TapestryInternalUtils.isEqual(columnId, sortColumnId))
                return ColumnSort.UNSORTED;

            return getColumnSort();
        }

        private ColumnSort getColumnSort() {
            return getSortAscending() ? ColumnSort.ASCENDING : ColumnSort.DESCENDING;
        }

        public void updateSort(String columnId) {
            assert InternalUtils.isNonBlank(columnId);
            if (columnId.equals(sortColumnId)) {
                setSortAscending(!getSortAscending());
                return;
            }

            sortColumnId = columnId;
            setSortAscending(true);
        }

        public List<SortConstraint> getSortConstraints() {
            if (sortColumnId == null) {
                return Collections.emptyList();
            }
            PropertyModel sortModel = getDataModel().getById(sortColumnId);

            SortConstraint constraint = new SortConstraint(sortModel, getColumnSort());

            return Collections.singletonList(constraint);
        }

        public void clear() {
            sortColumnId = null;
        }
    }
}
